Skip to content

Feat(client): 직무 아티클 주기적 업데이트 공지 로딩 추가#289

Merged
constantly-dev merged 4 commits intodevelopfrom
feat/#287/job-update-loading
Feb 26, 2026
Merged

Feat(client): 직무 아티클 주기적 업데이트 공지 로딩 추가#289
constantly-dev merged 4 commits intodevelopfrom
feat/#287/job-update-loading

Conversation

@constantly-dev
Copy link
Member

@constantly-dev constantly-dev commented Feb 26, 2026

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  • 직무 아티클 주기적 업데이트 공지 로딩 추가

⭐ PR Point (To Reviewer)

📷 Screenshot

2026-02-26.11.06.39.mov

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 채용 공고 목록의 하단에 도달할 때 알림 메시지가 표시됩니다.
    • 알림에는 "관심 직무 핀은 계속 업데이트 돼요!"라는 메시지와 함께 로딩 애니메이션이 포함됩니다.
    • 스크롤 동작에 반응하여 알림이 자동으로 표시되고 사라집니다.

@vercel
Copy link

vercel bot commented Feb 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pinback-client-client Ready Ready Preview, Comment Feb 26, 2026 2:12pm
pinback-client-landing Ready Ready Preview, Comment Feb 26, 2026 2:12pm

@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Feb 26, 2026
@github-actions
Copy link

github-actions bot commented Feb 26, 2026

✅ Storybook chromatic 배포 확인:
🐿️ storybook

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Walkthrough

관심 직무 리스트 페이지에 스크롤 시 하단 공지를 표시하는 기능을 추가합니다. 새로운 커스텀 훅으로 공지 표시 상태를 관리하고, 휠 이벤트를 처리하며, 로딩 스피너가 포함된 UI 컴포넌트를 렌더링합니다.

Changes

Cohort / File(s) Summary
페이지 로직 개선
apps/client/src/pages/jobPins/JobPins.tsx
로컬 ref를 useJobPinsBottomNotice 훅으로 대체하고, 휠 이벤트 핸들러를 추가하여 하단 공지 표시 기능 연결
상태 관리 훅
apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts
스크롤 컨테이너와 타이머를 관리하며, 휠 이벤트 처리 및 2초 임시 공지 표시 로직 구현
UI 컴포넌트
apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx, apps/client/src/pages/jobPins/components/JobPinsBottomSpinner.tsx
축소/확장 애니메이션을 지원하는 공지 컴포넌트와 SVG 기반 로딩 스피너 컴포넌트 추가

Sequence Diagram

sequenceDiagram
    actor User
    participant JobPins as JobPins Page
    participant Hook as useJobPinsBottomNotice
    participant Notice as JobPinsBottomNotice
    participant Spinner as JobPinsBottomSpinner

    User->>JobPins: 마우스 휠 스크롤
    JobPins->>Hook: handleBottomWheel(e) 호출
    Hook->>Hook: 스크롤 위치 확인<br/>(컨테이너가 하단인지)
    alt 하단 도달 시
        Hook->>Hook: showBottomNoticeTemporarily() 실행<br/>(isBottomNoticeVisible = true, 2초 타이머)
    end
    Hook-->>JobPins: isBottomNoticeVisible, scrollContainerRef 반환
    JobPins->>Notice: visible 상태 전달
    Notice->>Spinner: 중첩된 스피너 렌더링
    Spinner-->>Notice: 로딩 애니메이션 표시
    Notice-->>User: "관심 직무 핀은 계속 업데이트 돼요!" 공지 표시
    
    Note over Hook: 2초 후<br/>타이머 자동 정리<br/>isBottomNoticeVisible = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • jllee000
  • jjangminii

Poem

🐰 스크롤 내려 하단 닿으면
스피너 돌며 공지 나타나고
관심 직무는 계속 업뎃돼 🎡
무한 스크롤의 매력, 한층 업그레이드! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 내용의 핵심과 일치하며, 직무 아티클 주기적 업데이트 공지 로딩을 추가한다는 점을 명확히 표현합니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 있으며, 관련 이슈(#287)를 명시하고, 작업 내용과 스크린샷을 포함하고 있습니다.
Linked Issues check ✅ Passed PR은 #287의 목표인 관심 직무 리스트 업데이트 로딩 상태 구현을 충족하며, JobPinsBottomNotice 컴포넌트와 useJobPinsBottomNotice 훅으로 UI와 로딩 동작을 제공합니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 #287의 직무 업데이트 로딩 공지 구현과 관련되어 있으며, 범위를 벗어난 변경은 없습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#287/job-update-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Collaborator

@jjangminii jjangminii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿이에요 👍

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/client/src/pages/jobPins/components/JobPinsBottomSpinner.tsx (1)

7-14: 장식용 스피너는 접근성 트리에서 제외해 주세요.

Line 7의 <svg>는 정보 전달용이 아니라 시각 장식이라, 스크린리더 노출을 막는 편이 좋습니다 (aria-hidden, focusable).

제안 diff
     <svg
       xmlns="http://www.w3.org/2000/svg"
       width="30"
       height="30"
       viewBox="0 0 30 30"
       fill="none"
+      aria-hidden="true"
+      focusable="false"
       className="animate-spin-smooth h-[3rem] w-[3rem]"
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/components/JobPinsBottomSpinner.tsx` around
lines 7 - 14, The <svg> in JobPinsBottomSpinner (JobPinsBottomSpinner.tsx) is
decorative and should be removed from the accessibility tree: add
aria-hidden="true" and focusable="false" attributes to the <svg> element (ensure
there are no accessible child text/title elements), so screen readers won't
announce or focus this spinner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx`:
- Around line 9-15: The wrapper DIV that toggles visibility visually (the JSX
block in JobPinsBottomNotice where the className depends on `visible`) doesn't
update accessibility tree; add an `aria-hidden` attribute bound to the same
`visible` state (e.g., `aria-hidden={!visible}`) on that wrapper so assistive
technologies respect the hidden state, keeping the attribute logic consistent
with the existing `visible` conditional used in the className.

In `@apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts`:
- Around line 8-22: The early return in showBottomNoticeTemporarily prevents
updating the hide timer while the notice is already visible, so additional wheel
events won't extend the display; remove the return and always reset/clear
hideTimerRef and set a new timeout, ensuring you still call
setIsBottomNoticeVisible(true) when appropriate; update the logic in
showBottomNoticeTemporarily (referencing isBottomNoticeVisible, hideTimerRef,
setIsBottomNoticeVisible) to clear any existing hideTimerRef, set visibility if
not already true, and then assign a fresh setTimeout to hideTimerRef so repeated
calls extend the notice duration.

In `@apps/client/src/pages/jobPins/JobPins.tsx`:
- Around line 66-67: The sentinel re-enters while JobPinsBottomNotice animates
height (see JobPinsBottomNotice and the div using observerRef), causing
duplicate fetches because useInfiniteScroll only checks hasNextPage; update the
intersection callback inside useInfiniteScroll (or the place where fetchNextPage
is called) to guard with entry.isIntersecting and also check in-flight state
(either isFetching or better isFetchingNextPage passed in from TanStack Query)
before calling fetchNextPage so it only runs when intersecting and not already
fetching.

---

Nitpick comments:
In `@apps/client/src/pages/jobPins/components/JobPinsBottomSpinner.tsx`:
- Around line 7-14: The <svg> in JobPinsBottomSpinner (JobPinsBottomSpinner.tsx)
is decorative and should be removed from the accessibility tree: add
aria-hidden="true" and focusable="false" attributes to the <svg> element (ensure
there are no accessible child text/title elements), so screen readers won't
announce or focus this spinner.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e100421 and 4c34c48.

📒 Files selected for processing (4)
  • apps/client/src/pages/jobPins/JobPins.tsx
  • apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx
  • apps/client/src/pages/jobPins/components/JobPinsBottomSpinner.tsx
  • apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts

Comment on lines +9 to +15
<div
className={`w-full overflow-hidden transition-all duration-300 ease-out ${
visible
? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
: 'max-h-0 translate-y-2 py-0 opacity-0'
}`}
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

숨김 상태를 접근성 트리에도 반영해 주세요.

Line 10-14는 시각적 접힘만 처리해서, 숨김 상태 문구가 보조기기에 노출될 수 있습니다. 래퍼에 aria-hidden={!visible}를 같이 두는 게 안전합니다.

제안 diff
     <div
+      aria-hidden={!visible}
       className={`w-full overflow-hidden transition-all duration-300 ease-out ${
         visible
           ? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
           : 'max-h-0 translate-y-2 py-0 opacity-0'
       }`}
     >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className={`w-full overflow-hidden transition-all duration-300 ease-out ${
visible
? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
: 'max-h-0 translate-y-2 py-0 opacity-0'
}`}
>
<div
aria-hidden={!visible}
className={`w-full overflow-hidden transition-all duration-300 ease-out ${
visible
? 'max-h-[10rem] translate-y-0 py-[1.2rem] opacity-100'
: 'max-h-0 translate-y-2 py-0 opacity-0'
}`}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx` around
lines 9 - 15, The wrapper DIV that toggles visibility visually (the JSX block in
JobPinsBottomNotice where the className depends on `visible`) doesn't update
accessibility tree; add an `aria-hidden` attribute bound to the same `visible`
state (e.g., `aria-hidden={!visible}`) on that wrapper so assistive technologies
respect the hidden state, keeping the attribute logic consistent with the
existing `visible` conditional used in the className.

Comment on lines +8 to +22
const showBottomNoticeTemporarily = useCallback(() => {
if (isBottomNoticeVisible) {
return;
}

setIsBottomNoticeVisible(true);
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}

hideTimerRef.current = setTimeout(() => {
setIsBottomNoticeVisible(false);
hideTimerRef.current = null;
}, 2000);
}, [isBottomNoticeVisible]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

공지 재노출 타이머가 갱신되지 않습니다.

Line 9의 조기 return 때문에, 공지가 보이는 동안 추가 휠 입력이 와도 숨김 타이머가 연장되지 않습니다. 연속 스크롤 시 공지가 너무 빨리 꺼질 수 있습니다.

제안 diff
   const showBottomNoticeTemporarily = useCallback(() => {
-    if (isBottomNoticeVisible) {
-      return;
-    }
-
     setIsBottomNoticeVisible(true);
     if (hideTimerRef.current) {
       clearTimeout(hideTimerRef.current);
     }

     hideTimerRef.current = setTimeout(() => {
       setIsBottomNoticeVisible(false);
       hideTimerRef.current = null;
     }, 2000);
-  }, [isBottomNoticeVisible]);
+  }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const showBottomNoticeTemporarily = useCallback(() => {
if (isBottomNoticeVisible) {
return;
}
setIsBottomNoticeVisible(true);
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}
hideTimerRef.current = setTimeout(() => {
setIsBottomNoticeVisible(false);
hideTimerRef.current = null;
}, 2000);
}, [isBottomNoticeVisible]);
const showBottomNoticeTemporarily = useCallback(() => {
setIsBottomNoticeVisible(true);
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
}
hideTimerRef.current = setTimeout(() => {
setIsBottomNoticeVisible(false);
hideTimerRef.current = null;
}, 2000);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts` around lines 8
- 22, The early return in showBottomNoticeTemporarily prevents updating the hide
timer while the notice is already visible, so additional wheel events won't
extend the display; remove the return and always reset/clear hideTimerRef and
set a new timeout, ensuring you still call setIsBottomNoticeVisible(true) when
appropriate; update the logic in showBottomNoticeTemporarily (referencing
isBottomNoticeVisible, hideTimerRef, setIsBottomNoticeVisible) to clear any
existing hideTimerRef, set visibility if not already true, and then assign a
fresh setTimeout to hideTimerRef so repeated calls extend the notice duration.

Comment on lines +66 to 67
<JobPinsBottomNotice visible={isBottomNoticeVisible} />
<div ref={observerRef} style={{ height: '1px', width: '100%' }} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[1] useInfiniteScroll 구현 파일 탐색"
fd 'useInfiniteScroll\.(ts|tsx|js|jsx)$' || echo "No useInfiniteScroll hook file found by exact name"

echo
echo "[2] InfiniteScroll 관련 파일 탐색"
fd -t f -e ts -e tsx -e js -e jsx | xargs rg -l 'useInfiniteScroll|InfiniteScroll' | head -20

echo
echo "[3] JobPins 파일 내용 확인"
cat -n apps/client/src/pages/jobPins/JobPins.tsx | head -100

echo
echo "[4] JobPins 전체 구조"
wc -l apps/client/src/pages/jobPins/JobPins.tsx

Repository: Pinback-Team/pinback-client

Length of output: 3697


🏁 Script executed:

echo "[5] 디렉터리 구조 확인"
find apps/client/src -type f -name "*.ts" -o -name "*.tsx" | grep -i "infinite\|scroll\|hook" | head -20

Repository: Pinback-Team/pinback-client

Length of output: 676


🏁 Script executed:

echo "[6] useInfiniteScroll 또는 관련 훅 검색"
rg -t ts -t tsx 'export.*useInfiniteScroll|function useInfiniteScroll|const useInfiniteScroll' apps/

Repository: Pinback-Team/pinback-client

Length of output: 148


🏁 Script executed:

echo "[1] useInfiniteScroll 구현 확인"
cat -n apps/client/src/shared/hooks/useInfiniteScroll.ts

Repository: Pinback-Team/pinback-client

Length of output: 1509


🏁 Script executed:

echo "[2] JobPinsBottomNotice 컴포넌트 확인"
cat -n apps/client/src/pages/jobPins/components/JobPinsBottomNotice.tsx

Repository: Pinback-Team/pinback-client

Length of output: 1005


🏁 Script executed:

echo "[3] useJobPinsBottomNotice 훅 확인"
cat -n apps/client/src/pages/jobPins/hooks/useJobPinsBottomNotice.ts

Repository: Pinback-Team/pinback-client

Length of output: 1948


🏁 Script executed:

echo "[4] useGetJobPinsArticles 쿼리 함수 확인"
fd 'useGetJobPinsArticles' apps/ --exec cat -n {} \;

Repository: Pinback-Team/pinback-client

Length of output: 105


공지 높이 애니메이션으로 인한 센티넬 재교차 가능성이 있습니다.

Line 66-67에서 JobPinsBottomNotice가 300ms 전환 애니메이션으로 높이가 변하면(max-h: 0 ↔ 10rem), 센티넬이 재진입할 수 있습니다. 현재 useInfiniteScrollhasNextPage 가드만 사용하며, 옵저버 콜백 내부에 isFetching 체크가 없어 진행 중인 페치 중 레이아웃 변동 시 중복 호출 위험이 있습니다.

개선안: useInfiniteScroll 콜백에서 다음과 같이 추가 가드를 적용하세요.

if (entry.isIntersecting && hasNextPage) {
  fetchNextPage();
}

또는 TanStack Query의 isFetchingNextPage 상태를 props로 받아 콜백 내에서 체크하면 더욱 견고합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/jobPins/JobPins.tsx` around lines 66 - 67, The sentinel
re-enters while JobPinsBottomNotice animates height (see JobPinsBottomNotice and
the div using observerRef), causing duplicate fetches because useInfiniteScroll
only checks hasNextPage; update the intersection callback inside
useInfiniteScroll (or the place where fetchNextPage is called) to guard with
entry.isIntersecting and also check in-flight state (either isFetching or better
isFetchingNextPage passed in from TanStack Query) before calling fetchNextPage
so it only runs when intersecting and not already fetching.

@constantly-dev constantly-dev merged commit 234c03f into develop Feb 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 관심 직무 리스트 업데이트 텍스트/로딩 상태 구현

2 participants